/* CrowPanel 1.28" (ESP32-S3 + GC9A01, TFT_eSPI) Outer dial: glued labels, reversed order, −30 offset; world-grid ticks; smooth big-step tween. Inner dial: world-grid ticks + labels every 20th minor (60°) showing MHz with one decimal, computed from the actual frequency at that angle (matches the generator). by mircemk November 2025 */ #include #include #include #include #include #include "CST816D.h" //#include #include #define LED_PIN 48 #define LED_COUNT 5 #define LED_BRIGHTNESS 0 Adafruit_NeoPixel ring(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); int ledPos = 0; // current LED index #define UPPER_DIR (-1) // +1 = left→right increasing; -1 = reversed // --- Display power / init --- #define USE_PANEL_ENABLE_PINS 1 #define PIN_LCD_PWR_EN1 1 #define PIN_LCD_PWR_EN2 2 #define PIN_TFT_BL 46 #define PIN_TFT_RST 14 #define BL_CHANNEL 0 #define BL_FREQ 2000 #define BL_RES_BITS 8 #define FLASH_TIME_MS 200 // --- CrowPanel Touch Pins --- #define TP_I2C_SDA_PIN 6 #define TP_I2C_SCL_PIN 7 #define TP_RST 13 #define TP_INT 5 // --- Create Touch Object --- CST816D touch(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN, TP_RST, TP_INT); TFT_eSPI tft; static void panelPowerOn(){ if(USE_PANEL_ENABLE_PINS){ pinMode(PIN_LCD_PWR_EN1,OUTPUT); pinMode(PIN_LCD_PWR_EN2,OUTPUT); digitalWrite(PIN_LCD_PWR_EN1,HIGH); digitalWrite(PIN_LCD_PWR_EN2,HIGH); delay(5);} } static void pulseResetPin(){ pinMode(PIN_TFT_RST,OUTPUT); digitalWrite(PIN_TFT_RST,HIGH); delay(5); digitalWrite(PIN_TFT_RST,LOW); delay(10); digitalWrite(PIN_TFT_RST,HIGH); delay(20); } static void backlightInit(uint8_t duty){ pinMode(PIN_TFT_BL,OUTPUT); ledcSetup(BL_CHANNEL,BL_FREQ,BL_RES_BITS); ledcAttachPin(PIN_TFT_BL,BL_CHANNEL); ledcWrite(BL_CHANNEL,duty); } // --- IO pins --- #define ENC_A 45 #define ENC_B 42 #define ENC_BTN 41 #define SI5351_SDA 38 #define SI5351_SCL 39 uint32_t colors[] = { ring.Color(0, 0, 255), // blue ring.Color(0, 255, 255), // cyan ring.Color(255, 0, 255), // magenta ring.Color(255, 255, 0) // yellow }; // --- VFO / Si5351 --- static const uint64_t FREQ_MIN=10000ULL, FREQ_MAX=160000000ULL, FREQ_INIT=10100000ULL; static const int32_t SI5351_CORR_PPM=0; uint32_t stepLadder[]={10,100,1000,10000,100000,1000000}; uint8_t stepIndex=2; // 1 kHz uint64_t vfoHz=FREQ_INIT; Si5351 si5351; // ---------- Bands ---------- struct BandInfo { const char* name; uint64_t startFreq; const char* wavelength; }; BandInfo bands[] = { {"LW", 148500ULL, "2010 m"}, {"MW", 520000ULL, "577 m"}, {"SW 1", 1800000ULL, "160 m"}, {"SW 2", 3500000ULL, "80 m"}, {"SW 3", 5000000ULL, "60 m"}, {"SW 4", 7000000ULL, "40 m"}, {"SW 5", 10100000ULL, "30 m"}, {"SW 6", 14000000ULL, "20 m"}, {"SW 7", 18000000ULL, "17 m"}, {"SW 8", 21000000ULL, "15 m"}, {"SW 9", 24800000ULL, "12 m"}, {"SW10", 28000000ULL, "10 m"}, {"FM 1", 88000000ULL, "4 m"}, {"FM 2", 144000000ULL,"2 m"} }; int currentBand = 4; // start from SW 1 const int NUM_BANDS = sizeof(bands)/sizeof(bands[0]); // Compute band boundaries (end freq = next start - 1 Hz) uint64_t bandEndFreq(int idx) { if (idx >= NUM_BANDS - 1) return FREQ_MAX; return bands[idx + 1].startFreq - 1ULL; } // --- UI / geometry --- const int TOP_H=140, TOP_Y=0; int16_t CX=120, CY=120; TFT_eSprite spriteTop(&tft); // radii int16_t R_OUT_A=111, R_OUT_B=96; // outer dial int16_t R_IN_A = 70, R_IN_B =62; // inner dial // semicircle window (+ your -70° shift) float ANG0=-120.0f, ANG1=+120.0f; #define WINDOW_SHIFT_DEG (-70.0f) // tick grid const float TICK_MAJOR_STEP=30.0f; // majors each 30° (10 minors) const float TICK_MINOR_STEP=3.0f; // minors each 3° const int INNER_LABEL_EVERY_MINORS = 20; // label every 20 minors = 60° // lengths & thickness const int MINOR_LEN=4, MID_LEN=7, MAJOR_LEN=11; const int THICK_MINOR=1, THICK_MID=2, THICK_MAJOR=3; // mapping #define OUTER_DEG_PER_HZ (0.003f) // 30° = 10 kHz → 3° = 1 kHz #define INNER_RATIO (0.10f) // inner tape 10× slower (same dir) // colors #define COL_OUTER TFT_CYAN #define COL_INNER TFT_GREEN #define COL_CENTER TFT_RED #define COL_LABEL TFT_WHITE #define FRAME_COL TFT_DARKGREY // frame #define FRAME_R 118 #define FRAME_THICK 3 #define FRAME_POST_LEN 6 // encoder state int8_t encQuart=0; uint32_t lastBtnMs=0; volatile bool tweening=false; // ---------- helpers ---------- static inline float d2r(float d){ return d*PI/180.0f; } static inline float wrap360(float a){ a=fmodf(a,360.0f); if(a<0) a+=360.0f; return a; } static inline bool inWindow(float ang,float V0,float V1){ float span=V1-V0; float rel=wrap360(ang-V0); return rel<=span; } static inline int posmod(int a,int m){ int r=a%m; return (r<0)? r+m : r; } String formatKHzEU_2dec(uint64_t hz){ uint64_t khz_hundredths=(hz+5ULL)/10ULL; uint32_t frac2=(uint32_t)(khz_hundredths%100ULL); uint64_t khz_int=khz_hundredths/100ULL; char tmp[32]; snprintf(tmp,sizeof(tmp),"%llu",(unsigned long long)khz_int); String sInt(tmp), sSep; int n=sInt.length(); for(int i=0;i0 && (left%3)==0) sSep+='.'; } char buf[48]; snprintf(buf,sizeof(buf),"%s,%02u", sSep.c_str(), frac2); return String(buf); } void drawTickThick(TFT_eSprite* s,float angDeg,int16_t rOuter,int16_t rInner,uint16_t col,int thickness){ float ang=d2r(angDeg); float nx=-sinf(ang), ny=cosf(ang); for(int k=-(thickness/2); k<= (thickness/2); k++){ float offx=nx*k, offy=ny*k; int16_t x1=CX + rOuter*cosf(ang) + offx; int16_t y1=CY + rOuter*sinf(ang) + offy; int16_t x2=CX + rInner*cosf(ang) + offx; int16_t y2=CY + rInner*sinf(ang) + offy; s->drawLine(x1,y1,x2,y2,col); } } void drawTextAtAngle(TFT_eSprite* s,const String& txt,float ang,int16_t r,uint16_t col){ int16_t x=CX + r*cosf(d2r(ang)); int16_t y=CY + r*sinf(d2r(ang)); s->setTextDatum(MC_DATUM); s->setTextColor(col,TFT_BLACK); s->setTextFont(2); s->drawString(txt,x,y); } void drawThickArcSprite(int16_t r,float V0,float V1,uint16_t col,int thickness){ for(int t=-(thickness/2); t<= (thickness/2); t++){ float step=2.0f; for(float a=V0; a<=V1; a+=step){ int16_t x1=CX+(r+t)*cosf(d2r(a)), y1=CY+(r+t)*sinf(d2r(a)); float an=(a+step>V1)?V1:a+step; int16_t x2=CX+(r+t)*cosf(d2r(an)), y2=CY+(r+t)*sinf(d2r(an)); spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL); } } } // ---------- TOP (sprite) ---------- void drawTopScalesSprite(uint64_t hz){ const float V0=ANG0+WINDOW_SHIFT_DEG, V1=ANG1+WINDOW_SHIFT_DEG; // world “tapes” (angles) const float tapeOut = (float)hz * OUTER_DEG_PER_HZ; // deg const float tapeIn = (float)hz * OUTER_DEG_PER_HZ * INNER_RATIO;// deg spriteTop.fillSprite(TFT_BLACK); // ================= OUTER ticks ================= int nMin=(int)floorf((V0 - tapeOut)/TICK_MINOR_STEP) - 2; int nMax=(int)ceilf ((V1 - tapeOut)/TICK_MINOR_STEP) + 2; for(int n=nMin; n<=nMax; n++){ float ang=n*TICK_MINOR_STEP + tapeOut; if(!inWindow(ang,V0,V1)) continue; bool isMajor = (n % 10 == 0); bool mid = (!isMajor) && (n % 5 == 0); if(isMajor) continue; // majors drawn below drawTickThick(&spriteTop, ang, R_OUT_A, R_OUT_A - (mid?MID_LEN:MINOR_LEN), COL_OUTER, (mid?THICK_MID:THICK_MINOR)); } int mMin=(int)floorf((V0 - tapeOut)/TICK_MAJOR_STEP) - 1; int mMax=(int)ceilf ((V1 - tapeOut)/TICK_MAJOR_STEP) + 1; for(int m=mMin; m<=mMax; m++){ float ang = m*TICK_MAJOR_STEP + tapeOut; if(!inWindow(ang,V0,V1)) continue; drawTickThick(&spriteTop, ang, R_OUT_A, R_OUT_A - MAJOR_LEN, COL_OUTER, THICK_MAJOR); // glued label; reversed; −30 int tsel = (UPPER_DIR > 0) ? m : -m; int tens = posmod(tsel,10); tens = posmod(tens - 3, 10); char up[4]; snprintf(up, sizeof(up), "%d0", tens); drawTextAtAngle(&spriteTop, up, ang, R_OUT_B - 12, COL_LABEL); } // ================= INNER ticks ================= int niMin=(int)floorf((V0 - tapeIn)/TICK_MINOR_STEP) - 2; int niMax=(int)ceilf ((V1 - tapeIn)/TICK_MINOR_STEP) + 2; for(int n=niMin; n<=niMax; n++){ float ang=n*TICK_MINOR_STEP + tapeIn; if(!inWindow(ang,V0,V1)) continue; bool isMajor = (n % 10 == 0); bool mid = (!isMajor) && (n % 5 == 0); drawTickThick(&spriteTop, ang, R_IN_A, R_IN_A - (isMajor?MAJOR_LEN:(mid?MID_LEN:MINOR_LEN)), COL_INNER, (isMajor?THICK_MAJOR:(mid?THICK_MID:THICK_MINOR))); } // --------- NEW: INNER labels every 20 minors (60°), matching actual frequency --------- // For angles ai = k*60° + tapeIn (k integer), compute frequency at that angle: // f_at = hz + (ai - aCenter) / (OUTER_DEG_PER_HZ*INNER_RATIO) const float STEP_60 = TICK_MINOR_STEP * INNER_LABEL_EVERY_MINORS; // 60° const float aCenter = (V0 + V1) * 0.5f; int kMin = (int)floorf((V0 - tapeIn)/STEP_60) - 1; int kMax = (int)ceilf ((V1 - tapeIn)/STEP_60) + 1; for(int k=kMin; k<=kMax; k++){ float ai = k*STEP_60 + tapeIn; if(!inWindow(ai, V0, V1)) continue; // frequency at this angular position (round to 0.1 MHz) float deltaDeg = ai - aCenter; float deltaHz = -(deltaDeg) / (OUTER_DEG_PER_HZ * INNER_RATIO); int64_t f_at = (int64_t)hz + (int64_t)lroundf(deltaHz) - 100000LL; if (f_at < 0) f_at = 0; if (f_at > 160000000LL) f_at = 160000000LL; int64_t tenthsMHz = (f_at + 50000LL) / 100000LL; char lo[14]; snprintf(lo, sizeof(lo), "%lld.%01lld", (long long)(tenthsMHz / 10), (long long)(tenthsMHz % 10)); drawTextAtAngle(&spriteTop, String(lo), ai, R_IN_B - 15, COL_LABEL); } // ================= Frame + red line ================= // arc for (int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++){ float step=2.0f; for(float a=V0; a<=V1; a+=step){ int16_t x1=CX+(FRAME_R+t)*cosf(d2r(a)), y1=CY+(FRAME_R+t)*sinf(d2r(a)); float an=(a+step>V1)?V1:a+step; int16_t x2=CX+(FRAME_R+t)*cosf(d2r(an)), y2=CY+(FRAME_R+t)*sinf(d2r(an)); spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL); } } const int16_t xL=CX+FRAME_R*cosf(d2r(V0)), yL=CY+FRAME_R*sinf(d2r(V0)); const int16_t xR=CX+FRAME_R*cosf(d2r(V1)), yR=CY+FRAME_R*sinf(d2r(V1)); int16_t HLINE_Y = (((yL+yR)/2) - 1); if(HLINE_Y>(TOP_H-2)) HLINE_Y=TOP_H-2; for(int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++) spriteTop.drawLine(5, HLINE_Y+t, 235, HLINE_Y+t, FRAME_COL); spriteTop.drawLine(xL, HLINE_Y, xL, HLINE_Y - FRAME_POST_LEN, FRAME_COL); spriteTop.drawLine(xR, HLINE_Y, xR, HLINE_Y - FRAME_POST_LEN, FRAME_COL); // red center line const int16_t RED_TOP_Y=CY-105, RED_BOT_Y=HLINE_Y; for(int t=-1; t<=1; t++) spriteTop.drawLine(CX+t, RED_BOT_Y, CX+t, RED_TOP_Y, COL_CENTER); spriteTop.pushSprite(0, TOP_Y); } // ---------- bottom readout ---------- String formatKHzEU_2dec(uint64_t); // already defined above void drawFreqBox(uint64_t hz, uint32_t step) { // --- smaller frame, moved up --- int bx = 25; // reduced left margin (was 25) int by = 162; // 5 px below STEP label (was 180) int bw = 190; // narrower box (was 190) int bh = 36; int br = 6; // frame tft.drawRoundRect(bx, by, bw, bh, br, TFT_WHITE); tft.fillRoundRect(bx + 2, by + 2, bw - 4, bh - 4, br, TFT_BLACK); // --- frequency number --- tft.setTextDatum(ML_DATUM); tft.setTextColor(TFT_CYAN, TFT_BLACK); tft.setTextFont(4); String freqStr; String unitStr; char buf[32]; if (hz < 1000000ULL) { // Below 1 MHz → show in kHz, two decimals double kHz = hz / 1000.0; snprintf(buf, sizeof(buf), "%.2f", kHz); freqStr = String(buf); unitStr = "KHz"; } else if (hz < 100000000ULL) { // 1–99.999 MHz → show full kHz precision (like 11.880,00 MHz) double MHz = hz / 1000000.0; uint32_t whole = (uint32_t)MHz; uint32_t frac = (uint32_t)((MHz - whole) * 1000000.0 + 0.5); // Hz remainder // Format as ###.###,## (European style) snprintf(buf, sizeof(buf), "%lu.%03lu,%02lu", (unsigned long)whole, (unsigned long)(frac / 1000), (unsigned long)((frac / 10) % 100)); freqStr = String(buf); unitStr = "MHz"; } else { // ≥100 MHz → show simplified "M" unit double MHz = hz / 1000000.0; snprintf(buf, sizeof(buf), "%.2f", MHz); freqStr = String(buf); unitStr = "MHz"; } int textY = by + 2 + bh / 2; // frequency number tft.drawString(freqStr, bx + 8, textY); // --- unit label --- tft.setTextFont(4); tft.setTextColor(TFT_CYAN, TFT_BLACK); tft.setTextDatum(ML_DATUM); // place close to number (shift left ~15 px) tft.drawString(unitStr, bx + bw - 60, textY); // --- STEP label under gray frame line --- int stepY = 150; // adjust: ~5 px below the gray horizontal line int stepX = 120; // centered // clear old area tft.fillRect(60, stepY - 10, 120, 22, TFT_BLACK); // decide text char stepStr[12]; if (step == 10) strcpy(stepStr, "10 Hz"); else if (step == 100) strcpy(stepStr, "100 Hz"); else if (step == 1000) strcpy(stepStr, "1 KHz"); else if (step == 10000) strcpy(stepStr, "10 KHz"); else if (step == 100000) strcpy(stepStr, "100 KHz"); else if (step == 1000000) strcpy(stepStr, "1 MHz"); else snprintf(stepStr, sizeof(stepStr), "%lu Hz", (unsigned long)step); tft.setTextDatum(MC_DATUM); tft.setTextFont(2); tft.setTextColor(TFT_YELLOW, TFT_BLACK); tft.drawString(String("STEP: ") + stepStr, stepX, stepY); } void drawBottomFrameOnce(){ drawFreqBox(vfoHz, stepLadder[stepIndex]); } // ---------- encoder ---------- int8_t readEncoderTransition(){ static int last=0; int a=digitalRead(ENC_A), b=digitalRead(ENC_B); int val=(a<<1)|b; static const int8_t trans[16]={0,-1,+1,0,+1,0,0,-1,-1,0,0,+1,0,+1,-1,0}; int8_t d=trans[(last<<2)|val]; last=val; return d; } int8_t readEncoderDetent(){ if(tweening) return 0; int8_t t=readEncoderTransition(); if(t){ encQuart+=t; if(encQuart>=4){encQuart=0; return +1;} if(encQuart<=-4){encQuart=0; return -1;} } return 0; } // ---------- tween (unchanged from your good version) ---------- static inline uint32_t tweenSubstep(uint32_t stepHz){ if (stepHz >= 1000000)return 100000; // 1 MHz → 10×100 kHz if (stepHz >= 100000) return 10000; // 100 kHz → 10×10 kHz if (stepHz >= 10000) return 1000; // 10 kHz → 10×1 kHz return stepHz; } void updateBandFromFreq() { // check which band the current frequency belongs to for (int i = 0; i < NUM_BANDS; i++) { uint64_t start = bands[i].startFreq; uint64_t end = bandEndFreq(i); if (vfoHz >= start && vfoHz <= end) { if (currentBand != i) { bool up = (i > currentBand); // remember direction currentBand = i; drawBandInfo(stepLadder[stepIndex]); // redraw SW x / STEP / m info flashButton(up ? 1 : 0); // flash B+ if up, B– if down } break; } } } void applyTuningAndRender(int8_t clicks) { if (clicks == 0) return; uint32_t stepHz = stepLadder[stepIndex]; int64_t delta = (int64_t)clicks * (int64_t)stepHz; int64_t next = (int64_t)vfoHz + delta; if (next < (int64_t)FREQ_MIN) next = FREQ_MIN; if (next > (int64_t)FREQ_MAX) next = FREQ_MAX; uint64_t from = vfoHz; uint64_t to = (uint64_t)next; uint32_t sub = tweenSubstep(stepHz); if (sub < stepHz) { tweening = true; int dir = (to > from) ? +1 : -1; uint64_t cur = from; while (cur != to) { uint64_t nextStep = (dir > 0) ? (cur + sub) : (cur >= sub ? cur - sub : 0); if ((dir > 0 && nextStep > to) || (dir < 0 && nextStep < to)) nextStep = to; drawTopScalesSprite(nextStep); cur = nextStep; yield(); } vfoHz = to; } else { vfoHz = to; } si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); drawTopScalesSprite(vfoHz); drawFreqBox(vfoHz, stepHz); updateBandFromFreq(); // <-- new call here tweening = false; } // ---------- Si5351 ---------- bool siInit(){ Wire.begin(SI5351_SDA, SI5351_SCL, 400000); if(!si5351.init(SI5351_CRYSTAL_LOAD_8PF,0,SI5351_CORR_PPM)) return false; si5351.output_enable(SI5351_CLK0,1); si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); return true; } void drawBandInfo(uint32_t step) { int y = 150; // same baseline as STEP label tft.fillRect(10, y - 10, 220, 22, TFT_BLACK); tft.setTextFont(2); tft.setTextDatum(MC_DATUM); // Left part – band name (red) tft.setTextColor(TFT_YELLOW, TFT_BLACK); tft.drawString(bands[currentBand].name, 35, y); // Middle part – STEP label (yellow) tft.setTextColor(TFT_YELLOW, TFT_BLACK); char stepStr[12]; if (step == 10) strcpy(stepStr, "10 Hz"); else if (step == 100) strcpy(stepStr, "100 Hz"); else if (step == 1000) strcpy(stepStr, "1 KHz"); else if (step == 10000) strcpy(stepStr, "10 KHz"); else if (step == 100000) strcpy(stepStr, "100 KHz"); else if (step == 1000000) strcpy(stepStr, "1 MHz"); else sprintf(stepStr, "%lu Hz", (unsigned long)step); tft.drawString(String("STEP: ") + stepStr, 120, y); // Right part – wavelength (red) tft.setTextColor(TFT_YELLOW, TFT_BLACK); tft.drawString(bands[currentBand].wavelength, 200, y); } // ---------- setup / loop ---------- void setup(){ Serial.begin(115200); delay(100); ring.begin(); ring.setBrightness(LED_BRIGHTNESS); // soft brightness ring.clear(); ring.show(); panelPowerOn(); backlightInit(0); pulseResetPin(); tft.init(); tft.setRotation(0);tft.setRotation(0); // --- Turn on backlight gradually --- for (int d = 0; d <= 200; d += 10) { ledcWrite(BL_CHANNEL, d); delay(10); } // --- Splash / Intro Screen (before UI setup) --- tft.fillScreen(TFT_BLACK); tft.setTextDatum(MC_DATUM); tft.setTextFont(4); tft.setTextColor(TFT_YELLOW, TFT_BLACK); tft.drawString("Retro Style", 120, 90); tft.setTextColor(TFT_CYAN, TFT_BLACK); tft.drawString("VFO", 120, 120); tft.setTextFont(2); tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK); tft.drawString("by mircemk", 120, 150); delay(2000); tft.fillScreen(TFT_BLACK); // --- Now create sprites and draw the main UI --- spriteTop.setColorDepth(16); spriteTop.createSprite(240, TOP_H); spriteTop.setTextDatum(MC_DATUM); pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); pinMode(ENC_BTN, INPUT_PULLUP); tft.fillScreen(TFT_BLACK); drawTopScalesSprite(vfoHz); drawBottomFrameOnce(); drawTouchButtons(); if (siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); // --- Initialize and show the correct band immediately --- updateBandFromFreq(); drawBandInfo(stepLadder[stepIndex]); if(siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); // --- Initialize band display based on starting frequency --- updateBandFromFreq(); // detect which band 10.100 MHz belongs to drawBandInfo(stepLadder[stepIndex]); // draw SW5 30 m info immediately // --- Enable main panel power (required for touch rail) --- pinMode(1, OUTPUT); digitalWrite(1, HIGH); pinMode(2, OUTPUT); digitalWrite(2, HIGH); delay(20); // --- Reset and start the touch controller --- pinMode(TP_RST, OUTPUT); digitalWrite(TP_RST, LOW); delay(10); digitalWrite(TP_RST, HIGH); delay(50); Wire.begin(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN); touch.begin(); Serial.println("Touch initialized (CrowPanel 1.28)"); } void drawTouchButtons() { // Button geometry int btnW = 80, btnH = 36; int btnY = 205; // bottom area int btnLeftX = 35; // left button int btnRightX = 123; // right button uint16_t btnColor = tft.color565(255, 140, 0); // orange // --- LEFT (-1) --- tft.fillRoundRect(btnLeftX, btnY, btnW, btnH, 6, btnColor); tft.drawRoundRect(btnLeftX, btnY, btnW, btnH, 6, TFT_WHITE); tft.setTextDatum(MC_DATUM); tft.setTextFont(4); tft.setTextColor(TFT_WHITE, btnColor); tft.drawString("-B", btnLeftX + btnW/2 +10, btnY + btnH/2); // --- RIGHT (+1) --- tft.fillRoundRect(btnRightX, btnY, btnW, btnH, 6, btnColor); tft.drawRoundRect(btnRightX, btnY, btnW, btnH, 6, TFT_WHITE); tft.setTextDatum(MC_DATUM); tft.setTextFont(4); tft.setTextColor(TFT_WHITE, btnColor); tft.drawString("+B", btnRightX + btnW/2 - 10, btnY + btnH/2); } void flashButton(int btn) { // btn: 0 = B– , 1 = B+ int btnX = (btn == 0) ? 35 : 123; int btnY = 205; int btnW = 80, btnH = 36; // Flash red for 80 ms then return to orange uint16_t red = tft.color565(255, 0, 0); uint16_t orange = tft.color565(255, 140, 0); // show red tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, red); tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE); tft.setTextDatum(MC_DATUM); tft.setTextFont(4); tft.setTextColor(TFT_WHITE, red); tft.drawString((btn == 0) ? "-B" : "+B", btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2); delay(FLASH_TIME_MS); // back to orange tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, orange); tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE); tft.setTextColor(TFT_WHITE, orange); tft.drawString((btn == 0) ? "-B" : "+B", btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2); } // direction >0 → clockwise (right turn), direction <0 → counterclockwise void updateLedRing(int direction) { // reverse rotation logic so it matches encoder if (direction > 0) ledPos = (ledPos - 1 + LED_COUNT) % LED_COUNT; else if (direction < 0) ledPos = (ledPos + 1) % LED_COUNT; // draw single glowing yellow LED ring.clear(); ring.setPixelColor(ledPos, ring.Color(255, 180, 0)); // warm yellow ring.show(); } void loop() { int8_t det = readEncoderDetent(); if (det) applyTuningAndRender(det); updateLedRing(det); uint32_t now = millis(); bool pressed = (digitalRead(ENC_BTN) == LOW); if (!tweening && pressed && (now - lastBtnMs) > 250) { lastBtnMs = now; stepIndex = (stepIndex + 1) % (sizeof(stepLadder) / sizeof(stepLadder[0])); drawFreqBox(vfoHz, stepLadder[stepIndex]); } // --- Touch reading block (runs always) --- uint16_t x, y; uint8_t gesture; static uint32_t lastTouchMs = 0; if (millis() - lastTouchMs > 30) { // poll every 30 ms lastTouchMs = millis(); bool touched = touch.getTouch(&x, &y, &gesture); if (touched) { Serial.printf("Touch: X=%u Y=%u Gesture=0x%02X\n", x, y, gesture); if (y > 205 && y < 245) { // bottom strip only if (x >= 25 && x <= 115) { // left button flashButton(0); // fade red→orange if (currentBand > 0) currentBand--; } else if (x >= 125 && x <= 215) { // right button flashButton(1); if (currentBand < NUM_BANDS - 1) currentBand++; } vfoHz = bands[currentBand].startFreq; si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); drawTopScalesSprite(vfoHz); drawFreqBox(vfoHz, stepLadder[stepIndex]); drawBandInfo(stepLadder[stepIndex]); } } } delay(5); // watchdog-friendly pause }